# 从一个 俄罗斯方块看 Echarts 对 Zrender 的使用方式
来源还是以最近入职了一家新公司,让我使用 Zrender 来写一个俄罗斯方块,因为这方面比较陌生,所以给予 google 搜索了一下,基本都是以 Echarts 来进行的,所以不得意 查看 Zrender 的文档手撸了一个,在过程中发现 Zrender 的文档是写的真烂,有些还不是最新的,需要基于 源码 来进行推断 💩,最近正好有时间,所以看下 Echarts 是怎么对 Zrender 再次封装使用的。。。
- 话不多说,开始
<template>
<div class="tetris" ref="tetris"></div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import * as Echarts from 'echarts';
const tetris = ref(null);
onMounted(() => {
const myChat = Echarts.init(tetris.value!);
var refreshT: any, fallBlockT: any;
});
可以看到 Echarts.init
(opens new window) 来进行初始化的,并返回这个实例
export function init(
dom: HTMLElement,
theme?: string | object,
opts?: EChartsInitOpts
): EChartsType {
const isClient = !(opts && opts.ssr);
if (isClient) {
...
// 一堆判断逻辑
}
const chart = new ECharts(dom, theme, opts); // https://github.com/apache/echarts/blob/master/src/core/echarts.ts#L331
chart.id = 'ec_' + idBase++; // 是一个 时间戳
instances[chart.id] = chart; // instances 是一个 {[id: string]: ECharts}
// 这里给 dom 设置属性 -> _echarts_instance_="ec_1652617475180
isClient && modelUtil.setAttribute(dom, DOM_ATTRIBUTE_KEY, chart.id);
enableConnect(chart); // https://github.com/apache/echarts/blob/master/src/core/echarts.ts#L307:5
lifecycle.trigger('afterinit', chart); // https://github.com/apache/echarts/blob/master/src/core/lifecycle.ts#L66
// 注册 afterinit 函数位置:https://github.com/apache/echarts/blob/master/src/core/echarts.ts#L2785
return chart;
可以看到内部实例化了一个 ECharts
(opens new window),我们看 ECharts
类中构造函数 (opens new window)都干了什么?
class ECharts extends Eventful<ECEventDefinition> {
...
constructor(
dom: HTMLElement,
// Theme name or themeOption.
theme?: string | ThemeOption,
opts?: EChartsInitOpts
) {
// 实例化了一个关于 事件处理函数,通过查看文件发现是基于 zrender/src/core/Eventful 来的,可以看到其注释是用来 查询 事件来的
super(new ECEventProcessor());
...
// 基于 zrender 实例化了一个 zr
const zr = this._zr = zrender.init(dom, {
renderer: opts.renderer || defaultRenderer,
devicePixelRatio: opts.devicePixelRatio,
width: opts.width,
height: opts.height,
ssr: opts.ssr,
useDirtyRect: opts.useDirtyRect == null ? defaultUseDirtyRect : opts.useDirtyRect
});
...
theme && backwardCompat(theme as ECUnitOption, true); // 处理主题兼容性 https://github.com/apache/echarts/blob/master/src/preprocessor/backwardCompat.ts#L144
this._theme = theme;
this._locale = createLocaleObject(opts.locale || SYSTEM_LANG); // 合并默认配置 https://github.com/apache/echarts/blob/master/src/core/locale.ts#L55
// 基于 zrender/src/core/util 进行一系列事件操作 https://github.com/apache/echarts/blob/master/src/core/CoordinateSystem.ts#L28
this._coordSysMgr = new CoordinateSystemManager();
// https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/core/echarts.ts#L2474
const api = this._api = createExtensionAPI(this);
// Sort on demand
function prioritySortFunc(a: StageHandlerInternal, b: StageHandlerInternal): number {
return a.__prio - b.__prio;
}
// 使用 zrender https://github.com/ecomfe/zrender/blob/e25b11095c9861d0293a8a7d13199581942544f9/src/core/timsort.ts#L634
timsort(visualFuncs, prioritySortFunc);
timsort(dataProcessorFuncs, prioritySortFunc);
// 这也是一个操作调度器,封装了一些操作 https://github.com/apache/echarts/blob/master/src/core/Scheduler.ts#L102
this._scheduler = new Scheduler(this, api, dataProcessorFuncs, visualFuncs);
this._messageCenter = new MessageCenter();
// Init mouse events https://github.com/apache/echarts/blob/master/src/core/echarts.ts#L1045 可以看到是其内部私有方法,一些是基于 zrender.on ,一些是基于 内部实现的 trigger 来触发,最后使用 handleLegacySelectEvents 来处理消息中心一的事件
this._initEvents();
// In case some people write `window.onresize = chart.resize`
this.resize = bind(this.resize, this);
// 在这里调用了 zrender 的 动画监听器 https://github.com/ecomfe/zrender/blob/67c46f39c208bb86df84c0afa63399755747f4ff/src/animation/Animation.ts#L1
zr.animation.on('frame', this._onframe, this);
// 下面两个通过 zr.on 来监听渲染事件和移动事件和点击事件 https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/core/echarts.ts#L2009
bindRenderedEvent(zr, this);
bindMouseEvent(zr, this); // 这里内部 调用了 findComponentHighDownDispatchers 方法 https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/util/states.ts#L607:17
// ECharts instance can be used as value.
setAsPrimitive(this); // 这里调用了 zrender/src/core/util 的方法 https://github.com/ecomfe/zrender/blob/e25b11095c9861d0293a8a7d13199581942544f9/src/core/util.ts#L648
}
}
接着看 俄罗斯方块 实现代码,可以看到主要都是通过 myChat.setOption
这个方法来更新的
refreshT = function() {
var pts = (firstBlock.norBase as any).clone();
if (fallLine < 0 || touchFallOther()) {
...
myChat.setOption(option);
...
} else {
...
}
myChat.setOption(option);
};
那来看下 myChat.setOption
都在干什么,具体代码在 setOption
(opens new window),可以看到 有函数重载,针对不同的入参有不同的逻辑:
/**
* Usage:
* chart.setOption(option, notMerge, lazyUpdate);
* chart.setOption(option, {
* notMerge: ...,
* lazyUpdate: ...,
* silent: ...
* });
*
* @param opts opts or notMerge.
* @param opts.notMerge Default `false`.
* @param opts.lazyUpdate Default `false`. Useful when setOption frequently.
* @param opts.silent Default `false`.
* @param opts.replaceMerge Default undefined.
*/
// Expose to user full option.
// ECBasicOption -> https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/util/types.ts#L575
// ECUnitOption -> https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/util/types.ts#L519:13 可以看到关键的 interface 定义
setOption<Opt extends ECBasicOption>(option: Opt, notMerge?: boolean, lazyUpdate?: boolean): void;
setOption<Opt extends ECBasicOption>(option: Opt, opts?: SetOptionOpts): void;
/* eslint-disable-next-line */
setOption<Opt extends ECBasicOption>(option: Opt, notMerge?: boolean | SetOptionOpts, lazyUpdate?: boolean): void {
...
// 这里判断 this._model 不存在会重新初始化,因为后续步骤会用到这个,_model 是echart的私有变量,可以在 https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/core/echarts.ts#L350 看到,所以这个 _model 是在第一次 setOption 的时候设置的
if (!this._model || notMerge) {
// https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/model/OptionManager.ts#L58
const optionManager = new OptionManager(this._api);
const theme = this._theme;
// https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/model/Global.ts#L154
const ecModel = this._model = new GlobalModel();
ecModel.scheduler = this._scheduler;
ecModel.ssr = this._ssr;
// https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/model/Global.ts#L199
ecModel.init(null, null, null, theme, this._locale, optionManager);
}
this._model.setOption(option as ECBasicOption, { replaceMerge }, optionPreprocessorFuncs);
const updateParams = {
seriesTransition: transitionOpt,
optionChanged: true
} as UpdateLifecycleParams;
if (lazyUpdate) {
...
}
else {
try {
prepare(this);
updateMethods.update.call(this, null, updateParams);
}
catch (e) {
this[PENDING_UPDATE] = null;
this[IN_MAIN_PROCESS_KEY] = false;
throw e;
}
// Ensure zr refresh sychronously, and then pixel in canvas can be
// fetched after `setOption`.
if (!this._ssr) {
// not use flush when using ssr mode.
this._zr.flush();
}
this[PENDING_UPDATE] = null;
this[IN_MAIN_PROCESS_KEY] = false;
flushPendingActions.call(this, silent);
triggerUpdatedEvent.call(this, silent);
}
}
关键代码 :
// 1. 实际调用 https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/model/Global.ts#L214
// 1.2 接下来调用 https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/model/OptionManager.ts#L93:5 实际是 `optionManager` 下的 setOption 来重绘,
// 1.3. 在 `optionManager -> setOption` 下调用 zrdenr 的 each 方法进行遍历,并调用了 `zrender -> setAsPrimitive` 方法, 应该是是通过设置 `__ec_primitive__ = true` 来进行性能优化吧
// 1.4 使用 clone 对 optionObj 深拷贝来返回一个新的对象进行修改
// 1.5 使用 parseRawOption 对配置选项预处理 https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/model/OptionManager.ts#L300:10
// 1.6 内部针对 连续 多次 setoption 时 对 时间轴(timelineOptions) 和 移动端自适应(media) 特殊处理,不进行合并,而是直接替换为新值
// 1.7 最后 newParsedOption 配置 赋值到 _optionBackup 上
// 2. 最后 内部调用 `resetOption` 重置所有配置,跟传入参数 `recreate` 一致
this._model.setOption(option as ECBasicOption, { replaceMerge }, optionPreprocessorFuncs);
再看后续 else
的逻辑:
else {
try {
prepare(this);
updateMethods.update.call(this, null, updateParams);
}
catch (e) {
this[PENDING_UPDATE] = null;
this[IN_MAIN_PROCESS_KEY] = false;
throw e;
}
// Ensure zr refresh sychronously, and then pixel in canvas can be
// fetched after `setOption`.
if (!this._ssr) {
// not use flush when using ssr mode.
this._zr.flush();
}
this[PENDING_UPDATE] = null;
this[IN_MAIN_PROCESS_KEY] = false;
flushPendingActions.call(this, silent);
triggerUpdatedEvent.call(this, silent);
}
可以看到调用了
prepare
(opens new window) 方法,内部其实调用了_scheduler
(opens new window) 和prepareView
(opens new window),其中_scheduler
就是在 echarts 初始化方法中赋值的this._scheduler = new Scheduler(this, api, dataProcessorFuncs, visualFuncs);
;其中
prepareView
中使用eachSeries
(opens new window) 来处理series
相关数据,其中_componentsMap
(opens new window) 在创建的时候就已经默认声明了series
属性了关键代码
doPrepare
(opens new window):function doPrepare(model: ComponentModel): void { // By defaut view will be reused if possible for the case that `setOption` with "notMerge" // mode and need to enable transition animation. (Usually, when they have the same id, or // especially no id but have the same type & name & index. See the `model.id` generation // rule in `makeIdAndName` and `viewId` generation rule here). // But in `replaceMerge` mode, this feature should be able to disabled when it is clear that // the new model has nothing to do with the old model. const requireNewView = model.__requireNewView; // This command should not work twice. model.__requireNewView = false; // Consider: id same and type changed. const viewId = '_ec_' + model.id + '_' + model.type; let view = !requireNewView && viewMap[viewId]; if (!view) { const classType = parseClassType(model.type); // 如果 series 的 type 为 scatter, 那后续 new Clazz() 就是 new Scatter const Clazz = isComponent ? (ComponentView as ComponentViewConstructor).getClass(classType.main, classType.sub) : ( // FIXME:TS // (ChartView as ChartViewConstructor).getClass('series', classType.sub) // For backward compat, still support a chart type declared as only subType // like "liquidfill", but recommend "series.liquidfill" // But need a base class to make a type series. (ChartView as ChartViewConstructor).getClass(classType.sub) ); if (__DEV__) { assert(Clazz, classType.sub + ' does not exist.'); } view = new Clazz(); // 把 一些 扩展 api 挂在到当前实例上 view.init(ecModel, api); // 缓存下 viewMap[viewId] = view; viewList.push(view as any); // 添加到 画布上 zr.add(view.group); } model.__viewId = view.__id = viewId; view.__alive = true; view.__model = model; view.group.__ecComponentInfo = { mainType: model.mainType, index: model.componentIndex }; !isComponent && scheduler.prepareView( view as ChartView, model as SeriesModel, ecModel, api ); }
关键代码:
for (let i = 0; i < viewList.length;) { const view = viewList[i]; if (!view.__alive) { // 不是 活动的就销毁掉 并从 数组中移除 !isComponent && (view as ChartView).renderTask.dispose(); zr.remove(view.group); view.dispose(ecModel, api); viewList.splice(i, 1); if (viewMap[view.__id] === view) { delete viewMap[view.__id]; } view.__id = view.group.__ecComponentInfo = null; } else { i++; } }
接下来可以看到
updateMethods.update.call(this, null, updateParams);
(opens new window) 是一个 更新方法其中在 update 有一个方法
render(this, ecModel, api, payload, updateParams);
(opens new window),可以根据名称猜到这个应该是 渲染 相关的,其中render
(opens new window) 赋值在这里render = ( ecIns: ECharts, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload, updateParams: UpdateLifecycleParams ) => { allocateZlevels(ecModel); renderComponents(ecIns, ecModel, api, payload, updateParams); each(ecIns._chartsViews, function (chart: ChartView) { chart.__alive = false; }); renderSeries(ecIns, ecModel, api, payload, updateParams); // Remove groups of unrendered charts each(ecIns._chartsViews, function (chart: ChartView) { if (!chart.__alive) { chart.remove(ecModel, api); } }); };
有一些内部方法是基于 接口实现编程,比如
class CandlestickView extends ChartView {
(opens new window),这样就可以在 updateZ (opens new window) 直接使用了function updateZ(model: ComponentModel, view: ComponentView | ChartView): void { if (model.preventAutoZ) { return; } const z = model.get('z') || 0; const zlevel = model.get('zlevel') || 0; // Set z and zlevel view.eachRendered((el) => { doUpdateZ(el, z, zlevel, -Infinity); // Don't traverse the children because it has been traversed in _updateZ. return true; }); };
renderSeries
(opens new window) 可以看待注释是 渲染 图表 和 组件// 是调用 对应组建模块的 render 的方法 例如:https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/component/calendar/CalendarView.ts#L61 componentView.render(componentModel, ecModel, api, payload);
可以看到 有些内部常见
React
(opens new window) 是通过graphic.Rect
构建的:// render day rect _renderDayRect(calendarModel: CalendarModel, rangeData: CalendarParsedDateRangeInfo, group: graphic.Group) { const coordSys = calendarModel.coordinateSystem; const itemRectStyleModel = calendarModel.getModel('itemStyle').getItemStyle(); const sw = coordSys.getCellWidth(); const sh = coordSys.getCellHeight(); for (let i = rangeData.start.time; i <= rangeData.end.time; i = coordSys.getNextNDay(i, 1).time ) { const point = coordSys.dataToRect([i], false).tl; // every rect const rect = new graphic.Rect({ shape: { x: point[0], y: point[1], width: sw, height: sh }, cursor: 'default', style: itemRectStyleModel }); group.add(rect); } }
那么
graphic
这个里面是什么呢?,下面来看看一下util/graphic.ts
(opens new window)... import Circle from 'zrender/src/graphic/shape/Circle'; import Ellipse from 'zrender/src/graphic/shape/Ellipse'; import Sector from 'zrender/src/graphic/shape/Sector'; import Ring from 'zrender/src/graphic/shape/Ring'; import Polygon from 'zrender/src/graphic/shape/Polygon'; import Polyline from 'zrender/src/graphic/shape/Polyline'; import Rect from 'zrender/src/graphic/shape/Rect'; ...
可以看到是使用
zrender
里面方法构建对应数据的,至此关键代码基本完成,再次推荐一个插件方便查看源码:Sourcegraph (opens new window)